<?php
/*
 * Copyright 2011 <http://voidweb.com>.
 * Author: Deepesh Malviya <https://github.com/deepeshmalviya>.
 *
 * Simple-REST - Lightweight PHP REST Library
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

/**
 * Class implements RESTfulness
 */
class SimpleRest {

    private $request = array(); // Array storing request
    private $response;          // Array storing response
    private $controller;

    /**
     * Constructor
     * calls prepareRequest internally
     */
    public function __construct() {
        $this->prepareRequest();
    }

    /**
     * Function preparing the generic class variables describing the request from raw HTTP request headers & body.
     */
    private function prepareRequest() {
        global $debug;
        $resource = (isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO']) ? ltrim(rtrim($_SERVER['PATH_INFO'],'/'),'/') : false;
        $this->request['resource']    = $resource ? $resource : 'index';
        $this->request['method']      = strtolower($_SERVER['REQUEST_METHOD']);
        $this->request['script_name'] = $_SERVER['SCRIPT_NAME'];
        $this->request['server_name'] = $_SERVER['SERVER_NAME'];
        $this->request['server']      = 'http://' . $_SERVER['SERVER_NAME'];
        $this->request['headers']     = $this->getHeaders();
        $this->request['format']      = isset($_GET['format']) ? trim($_GET['format']) : null;
        switch($this->request['method']) {
            case 'get':
                $input = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
                $this->request['params'] = $_GET;
                break;
            case 'post':
            case 'put':
            case 'delete':
            default:
                $content_type = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '';
                $pos = strpos($content_type, "multipart/form-data"); #this indicates file upload
                if ($pos === false) {
                    $input = file_get_contents('php://input');
                }
                else {
                    $input = '';
                } 

                if ($input) {
                    # The input may be JSON encoded [{"id": 3, "field": "value", "num":77}] or query string encoded [id=3&field=value&num=77]
                    # There is a more rigorous way to detect the difference (request content-type header). It just seems simpler to look at the first character.
                    if ($input[0] === '{' || $input[0] === '[') {
                        $this->request['params'] = json_decode($input, true);
                        if ($this->request['params'] === false) throw new Exception('Error decoding input as JSON string', json_last_error());
                    }
                    else {
                        parse_str($input, $this->request['params']);
                    }
                    #if (strpos('application/x-www-form-urlencoded', $content_type) !== false) {
                    #}
                    #else {
                    #        #throw new Exception("Badly formatted input [$content_type] : [$input]", 405);
                    #}
                }
                else {
                    $this->request['params'] = $_REQUEST;
                }
                break;
        }
        if ($debug) {
            debug_print("##############################################################################\n");
            debug_print("# PARAMS: ".$this->request['method']."\n");
            debug_print("##############################################################################\n");
            debug_print('RAW');
            debug_print($input);
            debug_print('PARSED');
            debug_print(json_encode($this->request['params'],JSON_PRETTY_PRINT)."\n");
        }
        if (!function_exists('trim_value')) {
            function trim_value(&$value) {
                $value = trim($value);
            }
        }
        array_walk_recursive($this->request, 'trim_value');
    }

    /**
     * Function to resolve controller based on the resource name and http
     * method (GET/POST/PUT/DELETE) using reflection and get the response.
     * Passes the response to the response helpers class.
     */
    public function process() {
        global $options, $trace;
        if ($trace) trace_entry();
        try {
            $controllerName = $this->getController();
            if (null == $controllerName) {
                throw new Exception('Method Not Allowed', 405);
            }
            $controller = new ReflectionClass($controllerName);
            if (!$controller->isInstantiable()) {
                throw new Exception('Bad Request', 400);
            }
            try {
                $method = $controller->getMethod($this->request['method']);
            }
            catch (ReflectionException $re) {
                throw new Exception('Unsupported HTTP method ' . $this->request['method'], 405);
            }
            if (!$method->isStatic()) {
                $controller = $controller->newInstance($this->request);
                $this->controller = $controller;
                if (!$controller->checkAuth()) {
                    throw new Exception('Unauthorized', 401);
                }
                $method->invoke($controller);
                $this->response = $controller->getResponse();
                $this->responseStatus = $controller->getResponseStatus();
            }
            else {
                throw new Exception('Static methods not supported in Controllers', 500);
            }
            if (is_null($this->response)) {
                throw new Exception('Method Not Allowed', 405);
            }
        }
        catch (Exception $e) {
            $this->response = array('success' => false, 'code' => $e->getCode(), 'message' => $e->getMessage());
            if ($options['test_mode']) {
                $this->response['file'] = $e->getFile();
                $this->response['line'] = $e->getLine();
                $stack_trace = $e->getTraceAsString();
                if (preg_match('/\\n/', $stack_trace)) {
                    $this->response['stack_trace'] = explode("\n",$stack_trace);
                }
                else {
                    $this->response['stack_trace'] = $stack_trace;
                }
            }
        }
        $this->sendResponse();
        if ($trace) trace_exit();
    }

    /**
     * Function to resolve controller from anything noteworthy in the request.
     */
    private function getController() {
        global $options;
        $api_controller = (isset($options) && isset($options['api_controller'])) ? $options['api_controller'] : 'FrontAPIController';
        return $api_controller;
    }

    private function xmlHelper($data, $version = '1.0', $encoding = 'UTF-8') {
        $xml = new XMLWriter;
        $xml->openMemory();
        $xml->startDocument($version, $encoding);

        if (!function_exists('write')) {
            function write(XMLWriter $xml, $data, $old_key = null) {
                foreach($data as $key => $value){
                    if (is_array($value)){
                        if (!is_int($key)) {
                            $xml->startElement($key);
                        }
                        write($xml, $value, $key);
                        if (!is_int($key)) {
                            $xml->endElement();
                        }
                        continue;
                    }
                    // Special handling for integer keys in array
                    $key = (is_int($key)) ? $old_key.$key : $key;
                    $xml->writeElement($key, $value);
                }
            }
        }
        write($xml, $data);
        return $xml->outputMemory(true);
    }

    /**
     * Function to get HTTP headers
     */
    private function getHeaders() {
        if (function_exists('apache_request_headers')) {
            return apache_request_headers();
        }
        $headers = array();
        $keys = preg_grep('{^HTTP_}i', array_keys($_SERVER));
        foreach ($keys as $val) {
            $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($val, 5)))));
            $headers[$key] = $_SERVER[$val];
        }
        return $headers;
    }

    private static $codes = array(
            100 => 'Continue',
            101 => 'Switching Protocols',
            200 => 'OK',
            201 => 'Created',
            202 => 'Accepted',
            203 => 'Non-Authoritative Information',
            204 => 'No Content',
            205 => 'Reset Content',
            206 => 'Partial Content',
            300 => 'Multiple Choices',
            301 => 'Moved Permanently',
            302 => 'Found',
            303 => 'See Other',
            304 => 'Not Modified',
            305 => 'Use Proxy',
            306 => '(Unused)',
            307 => 'Temporary Redirect',
            400 => 'Bad Request',
            401 => 'Unauthorized',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            406 => 'Not Acceptable',
            407 => 'Proxy Authentication Required',
            408 => 'Request Timeout',
            409 => 'Conflict',
            410 => 'Gone',
            411 => 'Length Required',
            412 => 'Precondition Failed',
            413 => 'Request Entity Too Large',
            414 => 'Request-URI Too Long',
            415 => 'Unsupported Media Type',
            416 => 'Requested Range Not Satisfiable',
            417 => 'Expectation Failed',
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Timeout',
            505 => 'HTTP Version Not Supported'
        );

    private static $content_types = array(
            'adp' =>       'audio/adpcm',
            'au' =>        'audio/basic',
            'snd' =>       'audio/basic',
            'mid' =>       'audio/midi',
            'midi' =>      'audio/midi',
            'kar' =>       'audio/midi',
            'rmi' =>       'audio/midi',
            'mp4a' =>      'audio/mp4',
            'mpga' =>      'audio/mpeg',
            'mp2' =>       'audio/mpeg',
            'mp2a' =>      'audio/mpeg',
            'mp3' =>       'audio/mpeg',
            'm2a' =>       'audio/mpeg',
            'm3a' =>       'audio/mpeg',
            'oga' =>       'audio/ogg',
            'ogg' =>       'audio/ogg',
            'spx' =>       'audio/ogg',
            's3m' =>       'audio/s3m',
            'sil' =>       'audio/silk',
            'uva' =>       'audio/vnd.dece.audio',
            'uvva' =>      'audio/vnd.dece.audio',
            'eol' =>       'audio/vnd.digital-winds',
            'dra' =>       'audio/vnd.dra',
            'dts' =>       'audio/vnd.dts',
            'dtshd' =>     'audio/vnd.dts.hd',
            'lvp' =>       'audio/vnd.lucent.voice',
            'pya' =>       'audio/vnd.ms-playready.media.pya',
            'ecelp4800' => 'audio/vnd.nuera.ecelp4800',
            'ecelp7470' => 'audio/vnd.nuera.ecelp7470',
            'ecelp9600' => 'audio/vnd.nuera.ecelp9600',
            'rip' =>       'audio/vnd.rip',
            'weba' =>      'audio/webm',
            'aac' =>       'audio/x-aac',
            'aif' =>       'audio/x-aiff',
            'aiff' =>      'audio/x-aiff',
            'aifc' =>      'audio/x-aiff',
            'caf' =>       'audio/x-caf',
            'flac' =>      'audio/x-flac',
            'mka' =>       'audio/x-matroska',
            'm3u' =>       'audio/x-mpegurl',
            'wax' =>       'audio/x-ms-wax',
            'wma' =>       'audio/x-ms-wma',
            'ram' =>       'audio/x-pn-realaudio',
            'ra' =>        'audio/x-pn-realaudio',
            'rmp' =>       'audio/x-pn-realaudio-plugin',
            'wav' =>       'audio/x-wav',
            'xm' =>        'audio/xm',
            'cdx' =>       'chemical/x-cdx',
            'cif' =>       'chemical/x-cif',
            'cmdf' =>      'chemical/x-cmdf',
            'cml' =>       'chemical/x-cml',
            'csml' =>      'chemical/x-csml',
            'xyz' =>       'chemical/x-xyz',
            'bmp' =>       'image/bmp',
            'cgm' =>       'image/cgm',
            'g3' =>        'image/g3fax',
            'gif' =>       'image/gif',
            'ief' =>       'image/ief',
            'jpeg' =>      'image/jpeg',
            'jpg' =>       'image/jpeg',
            'jpe' =>       'image/jpeg',
            'ktx' =>       'image/ktx',
            'png' =>       'image/png',
            'btif' =>      'image/prs.btif',
            'sgi' =>       'image/sgi',
            'svg' =>       'image/svg+xml',
            'svgz' =>      'image/svg+xml',
            'tiff' =>      'image/tiff',
            'tif' =>       'image/tiff',
            'psd' =>       'image/vnd.adobe.photoshop',
            'uvi' =>       'image/vnd.dece.graphic',
            'uvvi' =>      'image/vnd.dece.graphic',
            'uvg' =>       'image/vnd.dece.graphic',
            'uvvg' =>      'image/vnd.dece.graphic',
            'sub' =>       'image/vnd.dvb.subtitle',
            'djvu' =>      'image/vnd.djvu',
            'djv' =>       'image/vnd.djvu',
            'dwg' =>       'image/vnd.dwg',
            'dxf' =>       'image/vnd.dxf',
            'fbs' =>       'image/vnd.fastbidsheet',
            'fpx' =>       'image/vnd.fpx',
            'fst' =>       'image/vnd.fst',
            'mmr' =>       'image/vnd.fujixerox.edmics-mmr',
            'rlc' =>       'image/vnd.fujixerox.edmics-rlc',
            'mdi' =>       'image/vnd.ms-modi',
            'wdp' =>       'image/vnd.ms-photo',
            'npx' =>       'image/vnd.net-fpx',
            'wbmp' =>      'image/vnd.wap.wbmp',
            'xif' =>       'image/vnd.xiff',
            'webp' =>      'image/webp',
            '3ds' =>       'image/x-3ds',
            'ras' =>       'image/x-cmu-raster',
            'cmx' =>       'image/x-cmx',
            'fh' =>        'image/x-freehand',
            'fhc' =>       'image/x-freehand',
            'fh4' =>       'image/x-freehand',
            'fh5' =>       'image/x-freehand',
            'fh7' =>       'image/x-freehand',
            'ico' =>       'image/x-icon',
            'sid' =>       'image/x-mrsid-image',
            'pcx' =>       'image/x-pcx',
            'pic' =>       'image/x-pict',
            'pct' =>       'image/x-pict',
            'pnm' =>       'image/x-portable-anymap',
            'pbm' =>       'image/x-portable-bitmap',
            'pgm' =>       'image/x-portable-graymap',
            'ppm' =>       'image/x-portable-pixmap',
            'rgb' =>       'image/x-rgb',
            'tga' =>       'image/x-tga',
            'xbm' =>       'image/x-xbitmap',
            'xpm' =>       'image/x-xpixmap',
            'xwd' =>       'image/x-xwindowdump',
            'eml' =>       'message/rfc822',
            'mime' =>      'message/rfc822',
            'igs' =>       'model/iges',
            'iges' =>      'model/iges',
            'msh' =>       'model/mesh',
            'mesh' =>      'model/mesh',
            'silo' =>      'model/mesh',
            'dae' =>       'model/vnd.collada+xml',
            'dwf' =>       'model/vnd.dwf',
            'gdl' =>       'model/vnd.gdl',
            'gtw' =>       'model/vnd.gtw',
            'mts' =>       'model/vnd.mts',
            'vtu' =>       'model/vnd.vtu',
            'wrl' =>       'model/vrml',
            'vrml' =>      'model/vrml',
            'x3db' =>      'model/x3d+binary',
            'x3dbz' =>     'model/x3d+binary',
            'x3dv' =>      'model/x3d+vrml',
            'x3dvz' =>     'model/x3d+vrml',
            'x3d' =>       'model/x3d+xml',
            'x3dz' =>      'model/x3d+xml',
            'appcache' =>  'text/cache-manifest',
            'ics' =>       'text/calendar',
            'ifb' =>       'text/calendar',
            'css' =>       'text/css',
            'csv' =>       'text/csv',
            'html' =>      'text/html',
            'htm' =>       'text/html',
            'js' =>        'text/javascript',
            'n3' =>        'text/n3',
            'txt' =>       'text/plain',
            'text' =>      'text/plain',
            'conf' =>      'text/plain',
            'def' =>       'text/plain',
            'list' =>      'text/plain',
            'log' =>       'text/plain',
            'in' =>        'text/plain',
            'dsc' =>       'text/prs.lines.tag',
            'rtx' =>       'text/richtext',
            'sgml' =>      'text/sgml',
            'sgm' =>       'text/sgml',
            'tsv' =>       'text/tab-separated-values',
            't' =>         'text/troff',
            'tr' =>        'text/troff',
            'roff' =>      'text/troff',
            'man' =>       'text/troff',
            'me' =>        'text/troff',
            'ms' =>        'text/troff',
            'ttl' =>       'text/turtle',
            'uri' =>       'text/uri-list',
            'uris' =>      'text/uri-list',
            'urls' =>      'text/uri-list',
            'vcard' =>     'text/vcard',
            'curl' =>      'text/vnd.curl',
            'dcurl' =>     'text/vnd.curl.dcurl',
            'scurl' =>     'text/vnd.curl.scurl',
            'mcurl' =>     'text/vnd.curl.mcurl',
            'sub' =>       'text/vnd.dvb.subtitle',
            'fly' =>       'text/vnd.fly',
            'flx' =>       'text/vnd.fmi.flexstor',
            'gv' =>        'text/vnd.graphviz',
            '3dml' =>      'text/vnd.in3d.3dml',
            'spot' =>      'text/vnd.in3d.spot',
            'jad' =>       'text/vnd.sun.j2me.app-descriptor',
            'wml' =>       'text/vnd.wap.wml',
            'wmls' =>      'text/vnd.wap.wmlscript',
            's' =>         'text/x-asm',
            'asm' =>       'text/x-asm',
            'c' =>         'text/x-c',
            'cc' =>        'text/x-c',
            'cxx' =>       'text/x-c',
            'cpp' =>       'text/x-c',
            'h' =>         'text/x-c',
            'hh' =>        'text/x-c',
            'dic' =>       'text/x-c',
            'f' =>         'text/x-fortran',
            'for' =>       'text/x-fortran',
            'f77' =>       'text/x-fortran',
            'f90' =>       'text/x-fortran',
            'java' =>      'text/x-java-source',
            'opml' =>      'text/x-opml',
            'p' =>         'text/x-pascal',
            'pas' =>       'text/x-pascal',
            'nfo' =>       'text/x-nfo',
            'etx' =>       'text/x-setext',
            'sfv' =>       'text/x-sfv',
            'uu' =>        'text/x-uuencode',
            'vcs' =>       'text/x-vcalendar',
            'vcf' =>       'text/x-vcard',
            '3gp' =>       'video/3gpp',
            '3g2' =>       'video/3gpp2',
            'h261' =>      'video/h261',
            'h263' =>      'video/h263',
            'h264' =>      'video/h264',
            'jpgv' =>      'video/jpeg',
            'jpm' =>       'video/jpm',
            'jpgm' =>      'video/jpm',
            'mj2' =>       'video/mj2',
            'mjp2' =>      'video/mj2',
            'mp4' =>       'video/mp4',
            'mp4v' =>      'video/mp4',
            'mpg4' =>      'video/mp4',
            'mpeg' =>      'video/mpeg',
            'mpg' =>       'video/mpeg',
            'mpe' =>       'video/mpeg',
            'm1v' =>       'video/mpeg',
            'm2v' =>       'video/mpeg',
            'ogv' =>       'video/ogg',
            'qt' =>        'video/quicktime',
            'mov' =>       'video/quicktime',
            'uvh' =>       'video/vnd.dece.hd',
            'uvvh' =>      'video/vnd.dece.hd',
            'uvm' =>       'video/vnd.dece.mobile',
            'uvvm' =>      'video/vnd.dece.mobile',
            'uvp' =>       'video/vnd.dece.pd',
            'uvvp' =>      'video/vnd.dece.pd',
            'uvs' =>       'video/vnd.dece.sd',
            'uvvs' =>      'video/vnd.dece.sd',
            'uvv' =>       'video/vnd.dece.video',
            'uvvv' =>      'video/vnd.dece.video',
            'dvb' =>       'video/vnd.dvb.file',
            'fvt' =>       'video/vnd.fvt',
            'mxu' =>       'video/vnd.mpegurl',
            'm4u' =>       'video/vnd.mpegurl',
            'pyv' =>       'video/vnd.ms-playready.media.pyv',
            'webm' =>      'video/webm',
            'f4v' =>       'video/x-f4v',
            'fli' =>       'video/x-fli',
            'flv' =>       'video/x-flv',
            'm4v' =>       'video/x-m4v',
            'mkv' =>       'video/x-matroska',
            'mk3d' =>      'video/x-matroska',
            'mks' =>       'video/x-matroska',
            'mng' =>       'video/x-mng',
            'asf' =>       'video/x-ms-asf',
            'asx' =>       'video/x-ms-asf',
            'vob' =>       'video/x-ms-vob',
            'wm' =>        'video/x-ms-wm',
            'wmv' =>       'video/x-ms-wmv',
            'wmx' =>       'video/x-ms-wmx',
            'wvx' =>       'video/x-ms-wvx',
            'avi' =>       'video/x-msvideo',
            'movie' =>     'video/x-sgi-movie',
            'smv' =>       'video/x-smv'
        );

    /**
     * Function returns HTTP response message based on HTTP response status code
     */
    private function getStatusMessage($status) {
        return (isset(self::$codes[$status])) ? self::$codes[$status] : self::$codes[500];
    }

    private function json_last_error () {
         $errorCode = json_last_error();
         if     ($errorCode == JSON_ERROR_DEPTH)            $message = 'The maximum stack depth has been exceeded';
         elseif ($errorCode == JSON_ERROR_STATE_MISMATCH)   $message = 'Invalid or malformed JSON';
         elseif ($errorCode == JSON_ERROR_CTRL_CHAR)        $message = 'Control character error, possibly incorrectly encoded';
         elseif ($errorCode == JSON_ERROR_SYNTAX)           $message = 'Syntax error';
         elseif ($errorCode == JSON_ERROR_UTF8)             $message = 'Malformed UTF-8 characters, possibly incorrectly encoded';
         elseif ($errorCode == JSON_ERROR_RECURSION)        $message = 'One or more recursive references in the value to be encoded';
         elseif ($errorCode == JSON_ERROR_INF_OR_NAN)       $message = 'One or more NAN or INF values in the value to be encoded';
         elseif ($errorCode == JSON_ERROR_UNSUPPORTED_TYPE) $message = 'A value of a type that cannot be encoded was given';
         else                                               $message = 'No error has occurred';
         return($message);
    }

    private function sendResponse() {
        global $trace;
        if ($trace) trace_entry();

        $controller = $this->controller;
        $filename = null;
        $filesize = null;
        if (isset($this->response)) {
            if (isset($controller->responseFilename)) {
                $filename = $controller->responseFilename;
                #$basename = basename($filename);
                #$filesize = filesize($filename);
                if (isset($controller->responseContentType)) {
                    $content_type = $controller->responseContentType;
                }
                elseif (preg_match('!\\.([^\\./]+)$!', $filename, $matches)) {
                    $file_ext = $matches[1];
                    if (isset(self::$content_types[$file_ext])) {
                        $content_type = self::$content_types[$file_ext];
                    }
                    else {
                        $content_type = 'application/octet-stream';
                    }
                }
                else {
                    $content_type = 'application/octet-stream';
                }
            }
            elseif (isset($controller->responseBody)) {
                $content_type = isset($controller->responseContentType) ? $controller->responseContentType : 'text/plain';
                $body         = $controller->responseBody;
            }
            else {
                $format  = isset($this->request['format']) ?  $this->request['format'] : 'json';
                if ($format === 'xml') {
                    $content_type = 'application/xml';
                    $body = $this->xmlHelper($this->response);
                }
                elseif ($format === 'qs') {
                    $content_type = 'text/plain';
                    $body = http_build_query($this->response);
                }
                else {   # ($format === 'json')
                    #$content_type = 'text/plain';
                    $content_type = 'application/json';
                    $body = json_encode($this->response);
                    if (!$body) {
                        $body = json_encode(array('success' => false, 'message' => $this->json_last_error()));
                    }
                }
            }
        }
        else {
            $content_type = 'querystring';
            $body         = '';
        }
        $status        = isset($this->responseStatus) ? $this->responseStatus : 200;
        $statusMessage = (isset(self::$codes[$status])) ? self::$codes[$status] : self::$codes[500];

        header("HTTP/1.1 $status $statusMessage");
        header('Content-Type: ' . $content_type);
        if (isset($filename)) {
            #header('Content-Length: ' . $filesize);
            #header('Content-Disposition: ' . $basename);
            ob_end_flush();       # end flushing

            if (isset($this->request['headers']['Range'])) {
                $range = $this->request['headers']['Range'];

                if (preg_match('/^bytes=(\d+)-(\d+)/', $range, $matches)) {
                    $range_offset = $matches[1];
                    $range_length = $matches[2] - $matches[1] + 1;

                    if ($range_length > 0) {
                        $sent_length = 0;
                        $max_read_length = 1024*1024;  # 1MB

                        $fh = @fopen($filename, "rb");
                        if ($fh === false) {
                            $message = "Unable to open file";
                            $error = error_get_last();
                            if (isset($error) && isset($error['message'])) {
                                $errmsg = $error['message'];
                                if (preg_match('/^.*: *(.*)$/', $errmsg, $matches)) {
                                    $errmsg = $matches[1];
                                }
                                $message .= ": $errmsg";
                            }
                            # TODO: What do I do with the message? And how do I signal an HTTP error?
                        }
                        else {
                            if ($range_offset) fseek($fh, $range_offset);

                            while (!feof($fh) && $sent_length < $range_length) {
                                $read_length = ($range_length - $sent_length > $max_read_length) ? $max_read_length : ($range_length - $sent_length);
                                $buf = fread($fh, $read_length);
                                echo $buf;
                                $sent_length += $read_length;  # if we hit EOF, this will be inflated. (didn't want to do strlen($buf))
                            }

                            fclose($fh);
                        }
                    }
                }
                else {
                    readfile($filename);  # sends file contents directly to STDOUT
                }
            }
            else {
                readfile($filename);  # sends file contents directly to STDOUT
            }
        }
        else {
            echo $body;
        }
        if ($trace) trace_exit();
    }
}

/**
 * Abstract Controller
 * To be extended by every controller in application
 */
abstract class SimpleRestController {
    protected $request;
    protected $response;
    public    $responseStatus;
    public    $responseContentType;
    public    $responseFilename;
    public    $responseBody;

    public function __construct($request) {
        $this->request = $request;
    }

    final public function getResponseStatus() {
        return $this->responseStatus;
    }

    final public function getResponse() {
        return $this->response;
    }

    public function checkAuth() {
        return true;
    }

    // @codeCoverageIgnoreStart
    abstract public function get();
    abstract public function post();
    abstract public function put();
    abstract public function delete();
    // @codeCoverageIgnoreEnd
    public function __toString() {
        $str = <<<EOF
SimpleRestController
 this->responseStatus=$this->responseStatus
 this->responseContentType=$this->responseContentType
 this->responseBody=$this->responseBody
 this->responseFilename=$this->responseFilename
EOF;
        return($str);
    }
}
